Claude/feature llm moderation posts - #14
Merged
Merged
Conversation
…fication Three new modules join the existing activities module on a unified "广场": - POST: general dynamics — any VERIFIED+ member - MEDICAL: hospital/doctor reviews — any VERIFIED+ posts, all comment - RESOURCE: skills on offer or requests for help — VERIFIED+ posts Activities are tightened to TRUSTED+ to match their higher trust profile. Every post, comment, and event now passes through containsBlockedTerms (cheap regex backstop) and then moderateAndClassify, which calls an Anthropic-compatible /v1/messages endpoint (e.g. Deepseek). The LLM returns a verdict (pass/flag/reject), a user-facing reason, the true section the content belongs in, and risk categories. Verdict drives status: pass->PUBLISHED, flag->PENDING_REVIEW (admin queue), reject->400 with reason. Section mismatch (e.g. an "event" posted as POST) is rejected with guidance. The service fails closed: any network/parsing error becomes a flag so content is parked for human review rather than silently dropped. D1 schema 0005 adds the posts table, the moderation result columns on both posts and events, and rebuilds comments to allow polymorphic event_id XOR post_id. Routes added: - GET/POST /api/posts, GET/PATCH/DELETE /api/posts/:id - GET/POST /api/posts/:id/comments, PATCH /api/posts/comments/:cid/hide - GET /api/posts/admin/pending, PATCH /api/posts/:id/review Frontend adds /posts list (tab-filtered by section), /posts/new, /posts/[id] (with pending-review banner for the author/admin), /admin/posts review queue, and a "广场" entry in TopBar/MobileNav. Vitest covers the LLM parser and fail-closed branches (13 tests). Integration verified locally end-to-end against Deepseek v4-flash: benign passes, keyword spam rejected at the door, hookup content rejected by LLM, event-shaped content from non-TRUSTED rejected with guidance, MEDICAL+RESOURCE posts published, comments moderated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This revision makes the user experience around posts/events flow purely through notifications + email. The frontend no longer exposes any moderation surface or asks the user to pick a section — the LLM classifies + tags + decides verdict; users just write title/body. Changes: - LLM service (worker/src/lib/llm.ts) now produces a controlled tag vocabulary alongside verdict/section/categories. The prompt is rewritten to treat medical-science / medical-experience as first-class citizens (no longer require "specific hospital name") and to be honest about the EVENT-tier rule. A new generateXiaoTReply produces a warm, bounded reply for question/help posts. - New retry helper (worker/src/lib/retry.ts) with exponential backoff 500/1500/3000ms, applied around both moderation and 小T calls. Failures still close down to flag→PENDING_REVIEW so nothing slips through. - Migration 0006 introduces a 小T system user (id 'system-xiao-t', handle 'xiao_t') and adds is_bot to comments so the UI can label machine-generated replies. - POST /api/posts always returns 200 with status; the verdict path records the row in REJECTED/PENDING_REVIEW state, writes a notification in every case, and emails the author for non-PUBLISHED outcomes. Same async pattern applied to event creation. Activities no longer require an onlineUrl up-front (organizer can share the link later). - Posts list page now switches by tag (全部 / 提问求助 / 医疗 / 资源 / 分享). PostForm shrinks to title/body/visibility — the LLM derives everything else. - Notifications page learns POST_APPROVED / POST_PENDING_REVIEW / POST_REJECTED / XIAO_T_REPLIED / EVENT_APPROVED / EVENT_PENDING_REVIEW / EVENT_REJECTED. - 22 vitest cases cover parser robustness, retry semantics, fail-closed paths, 小T trimming + sustained-failure handling. Integration tested end-to-end against deepseek-v4-flash: question-help post → 小T comment landed in seconds, paired with POST_APPROVED and XIAO_T_REPLIED notifications. Medical-science post without hospital name now publishes. Hookup content is saved REJECTED, hidden from public, notified to the author. Online event with no link publishes correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier draft tried to auto-demote LLM-classified EVENT content to a regular post. That's the wrong move: activities are a high-trust channel with its own tier gate, and silently turning an activity solicitation into a casual post would let GUEST/VERIFIED users smuggle event coordination into the broader feed. Now: - EVENT-classified content from /api/posts is rejected outright with a reason that explains the tier requirement and points at the activities module. The post is still saved in REJECTED state so the author keeps a record and is notified. - POST/MEDICAL/RESOURCE classification continues to happen automatically — users only write title+body, the LLM picks the section and the tag. Verified end-to-end against deepseek-v4-flash: a hormone-self-test post lands in MEDICAL with medical-science tag; a "I can review your résumé" offer lands in RESOURCE with resource-offer tag; an event-organising post is rejected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Posts board now actively refuses to host event coordination, with the exact response depending on the author's tier and the completeness of the LLM-extracted event details: - < TRUSTED: post is not saved. Author is notified (POST_BLOCKED_EVENT) with an explanation that activities require TRUSTED+ and a hint to ask an organizer to host on their behalf. Email goes out. - TRUSTED+ AND draft is complete (title/description/category/format/ start/end, plus city for offline/hybrid): a new activity is created in /api/activities directly from the draft. Author gets POST_RELOCATED_TO_EVENT pointing at the new event page; no post row is created. - TRUSTED+ AND draft is incomplete: the post is saved in PENDING_REVIEW state so the author keeps their text. The POST_NEEDS_EVENT_INFO notification lists exactly which fields are missing (开始时间、城市…), and an email links back to the post. To make this work, the LLM prompt was extended with a fifth output slot — eventDraft / eventDraftComplete / eventDraftMissing — and the moderation max_tokens was raised from 500 to 1500 so deepseek's thinking block plus a fully-spec'd draft fit without truncation. The completeness check is double-validated: even if the model says complete=true, we recompute and downgrade if obvious gaps remain. Integration verified end-to-end against deepseek-v4-flash: "6月21日周日下午两点上海徐家汇桌游聚会" by a TRUSTED user got auto-relocated to a SOCIAL/OFFLINE activity on 2026-06-21 14:00→16:00 with city=上海, preciseAddr=徐家汇市民活动中心二楼212室, capacity=20. A vague "想组织 一次聚会" by the same user was held back with a notification listing [开始时间, 结束时间, 城市] as missing. An EVENT-shaped post by a VERIFIED-only user was refused outright with no DB row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous EVENT handling rejected VERIFIED users for *any* post the LLM classified as EVENT — even harmless "anyone been to last week's hike?" questions. The LLM now distinguishes intent: - ORGANIZING: author is actively calling people to attend a specific new activity (time/place/sign-up signals). Activities are gated to TRUSTED+, so the same three-tier flow as before applies: refuse for VERIFIED, auto-create activity for TRUSTED+ when the draft is complete, hold for completion otherwise. - DISCUSSING: author is sharing, retrospecting, asking or discussing. Treated as a normal POST and saved with the LLM's chosen tag (share-life / question-help / announcement / ...). Default-to-DISCUSSING when the model omits intent — the cost of letting a conversation through is much lower than the cost of blocking it. Also: - max_tokens raised to 9000 so deepseek's thinking block has room to reason about the new schema without truncating JSON. - /posts/:id detail page gets an Edit button (author/admin) and a Report button (everyone else). Reports of type POST are accepted by the worker (added to VALID_TYPES). - New /posts/:id/edit page reuses the simplified PostForm in edit mode. Saves go through the same LLM moderation flow as creates. Integration verified end-to-end with deepseek-v4-flash: - "昨天的桌游聚会很有意思" by VERIFIED → PUBLISHED, tag share-life - "有没有人参加过北京上周的姐妹徒步活动" by VERIFIED → PUBLISHED, tag question-help - "组织本周六线下聚会" by VERIFIED → BLOCKED (still gated) - 32 unit tests pass, including three new ones covering the intent split. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two big additions land together.
== LLM auto-handles incoming reports ==
worker/src/lib/llm.ts adds evaluateReport(): given a target's
text + the reporter's reason, returns a verdict (valid|invalid|
uncertain) and an action (hide_target|dismiss|escalate).
worker/src/routes/reports.ts runs this asynchronously after the
report is recorded, then:
- hide_target -> hides POST (status=HIDDEN), EVENT (CANCELLED) or
COMMENT (is_hidden=1); marks report RESOLVED;
notifies both reporter (REPORT_AUTO_RESOLVED) and
the content author (CONTENT_HIDDEN_BY_REPORT).
- dismiss -> marks report DISMISSED; notifies the reporter
(REPORT_AUTO_DISMISSED).
- escalate -> leaves the report OPEN for the admin queue with
the LLM's hint stashed in resolved_note; notifies
the reporter (REPORT_RECEIVED).
USER-type reports always escalate — we never let the model auto-
suspend an account. Failure to reach the LLM also escalates.
Verified end-to-end against deepseek-v4-flash:
- "我不喜欢爬山照片" report on a benign post → DISMISSED
- Report on an obvious "加微信办证" post → AUTO HIDDEN
- USER-type report → escalated, OPEN
== Posts / comments feature completeness ==
- Comment authors can self-delete their own comments
(DELETE /api/posts/comments/:cid). Nested replies cascade.
- CommentSection rewritten: nested tree rendering with one-level
indent, inline Reply / Delete buttons, and a load-more button
driven by cursor-based pagination (?cursor=created_at).
- Posts list grows a search input that drives ?q=, plus a
"load more" button driven by ?page= pagination.
- New schema (migration 0007): post_likes, post_bookmarks,
subscriptions(kind, ref) for following authors or tags.
- Worker endpoints: POST /api/posts/:id/{like,bookmark} toggles,
GET /api/posts/me/bookmarks, GET /api/posts/me/drafts,
GET/POST/DELETE /api/users/me/subscriptions.
- Posts board supports drafts via POST /api/posts?draft=1; saves
status=DRAFT, skips moderation, only visible to the author.
- PostForm gains an image-upload button (uses existing /api/files
endpoint, inserts  at cursor) and a "存为草稿" action.
- Detail page renders body via react-markdown + rehype-sanitize
(links/markdown render natively without any extra plumbing).
- Detail page adds Like, Bookmark, Follow-author and Report
buttons with optimistic updates and toast feedback.
- New /me/bookmarks and /me/drafts pages reachable from UserMenu.
- /notifications page handles all the new kinds.
37 vitest cases pass (added evaluateReport coverage:
USER-escalates, fail-closed, valid/invalid parsing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audited the posts/reports/likes/bookmarks/subscriptions surfaces and
fixed the issues that actually mattered.
Backend correctness:
- likes/bookmarks toggle now uses INSERT OR IGNORE + meta.changes to
decide insert-vs-delete, so rapid concurrent clicks can't crash on a
UNIQUE-violation. Both endpoints also require the post to exist and
be visible to the viewer (a hidden/rejected post no longer
accumulates phantom engagement).
- GET /api/posts/:id/comments now gates on the parent post's status +
visibility. Hidden / draft / pending posts no longer leak their
comments via direct API call.
- Posts list query folds comment_count and like_count into a single
LEFT JOIN-on-derived-table instead of running N subqueries per page
row.
- Visibility WHERE clause now parameterizes viewer.id (was string-
concatenated); the ADMIN check is also explicit. Same shape as a
helper returning { sql, params } for clean reuse.
- Reports route: if evaluateReport says hide_target but hideTarget()
fails (target deleted between report and eval), the report no longer
silently sits in OPEN. It escalates to admin with a note about the
failure, and the reporter gets REPORT_RECEIVED rather than a false
REPORT_AUTO_RESOLVED.
- Auth: system accounts (id starts with 'system-', e.g. xiao_t) cannot
log in. Magic-link verify + TOTP login + password login each get an
isSystemUser() guard returning 403. They exist only to author bot
content.
Frontend polish:
- PostCard renders a like count alongside the comment count, and shows
a DRAFT badge for the author's own draft entries.
- /me/drafts cards link the whole card body to /posts/:id/edit (draft
detail page would 404 for non-authors, so detail was the wrong
landing). Delete moves to a separate ghost button so it isn't a
swipe-target inside the link.
- Like / bookmark / subscribe handlers gain a useRef-based "pending"
lock so rapid double-clicks can't fire concurrent toggles and end
the UI out-of-sync. Servers reconcile to whatever the response says.
- CommentSection caps visual nesting at depth 4 — deeper replies still
render but stop indenting so the layout doesn't slide off the edge.
- MobileNav adds safe-area bottom padding, truncate on labels, and
min-w-0 columns so 5 items fit on 320px screens without overflow.
- Notification page replaces the raw n.kind fallback with a friendly
"你收到一条系统通知" + a console.warn so new kinds don't ship looking
like SHOUTY_SNAKE_CASE.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The events list query had the same string-concatenation pattern for
viewer.id that posts.ts already had — even with viewer.id sourced from
a trusted JWT, mixing concatenation with parameterization invites
future-bug surface area. Lift to the same { sql, params } helper
shape posts.ts uses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix invite consumption flow tests and update documentation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
transcommunity | 638140e | Commit Preview URL Branch Preview URL |
May 24 2026, 03:34 PM |
Removed conditional checks for specific branches in build and deploy jobs.
Contributor
Author
|
@copilot resolve all frontend build errors and api backend errors to make new features completely integrate |
Agent-Logs-Url: https://github.com/TransHistoria/community/sessions/b6b02699-ebd1-4acb-b465-5c6e4cb1a56f Co-authored-by: cyanmint <20249732+cyanmint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/TransHistoria/community/sessions/b6b02699-ebd1-4acb-b465-5c6e4cb1a56f Co-authored-by: cyanmint <20249732+cyanmint@users.noreply.github.com>
…in/posts frontend route Agent-Logs-Url: https://github.com/TransHistoria/community/sessions/a41c9e14-00b4-4976-9297-34e48a27c246 Co-authored-by: cyanmint <20249732+cyanmint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/TransHistoria/community/sessions/de283163-f9c9-41b0-a357-6809b661fa8f Co-authored-by: cyanmint <20249732+cyanmint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/TransHistoria/community/sessions/ddeb23be-1b76-4ef2-8f49-7f93115a9bdc Co-authored-by: cyanmint <20249732+cyanmint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/TransHistoria/community/sessions/bf36a477-eea7-413e-8dc6-c4e4ff005eba Co-authored-by: cyanmint <20249732+cyanmint@users.noreply.github.com>
Agent-Logs-Url: https://github.com/TransHistoria/community/sessions/bf36a477-eea7-413e-8dc6-c4e4ff005eba Co-authored-by: cyanmint <20249732+cyanmint@users.noreply.github.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
transcommunity | 099ace0 | May 24 2026, 04:49 PM |
…ntacts dashboard Agent-Logs-Url: https://github.com/TransHistoria/community/sessions/da8ed36d-042b-4d6f-a1d3-9564dc85b298 Co-authored-by: cyanmint <20249732+cyanmint@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.